package com.happy3w.bean.factory;

import com.happy3w.java.ext.ListUtils;
import com.happy3w.java.ext.StringUtils;
import com.happy3w.toolkits.message.MessageRecorderException;
import com.happy3w.toolkits.utils.TypeUtil;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Slf4j
public class DynBeanFactory<A extends BeanConfig<C>, C extends BeanConfigItem, B> {
    /**
     * 整个配置文件对应数据结构
     */
    protected final Class<A> beanConfigType;

    /**
     * 单个管理单元对应配置类型基类
     */
    protected final Class<C> itemConfigType;

    /**
     * 单个管理单元对应实例类型基类
     */
    protected final Class<B> beanType;

    protected final ConfigBeanHelper configBeanHelper;

    /**
     * 从Json中解码各种类型的工具
     */
    protected final DecodeHelper decodeHelper;

    /**
     * 加载后的完整配置
     */
    protected A beanConfig;

    /**
     * 已经加载的各个实例配置
     */
    protected final Map<String, C> beanConfigs = new HashMap<>();

    /**
     * 共享的bean实例
     */
    private final Map<String, B> beans = new ConcurrentHashMap<>();

    public DynBeanFactory(Class<C> itemConfigType,
                          Class<B> beanType,
                          ConfigBeanHelper configBeanHelper) {
        this.configBeanHelper = configBeanHelper;
        this.beanConfigType = getBeanConfigType();
        this.itemConfigType = itemConfigType;
        this.beanType = beanType;
        this.decodeHelper = configBeanHelper.getDecodeHelper();
    }

    protected Class<A> getBeanConfigType() {
        Class firstType = TypeUtil.getType(this.getClass(), DynBeanFactory.class, 0);
        Class resultType = firstType == null ? BeanConfig.class : firstType;
        return resultType;
    }

    public void initConfigType(String packageName, Class... configTypes) {
        configBeanHelper.initConfigType(packageName, configTypes);
    }

    public void initBeanTypes(String... packageNames) {
        configBeanHelper.initBeanTypes(itemConfigType, beanType, packageNames);
    }

    public void loadConfig(String configName) {
        beanConfigs.clear();
        this.beanConfig = decodeHelper.loadFromFile(new File(configName), getBeanConfigType());
        beanConfig.getItems().forEach(item -> {
            if (item.getEnabled() == null) {
                item.setEnabled(true);
            }
            if (beanConfigs.containsKey(item.getName())) {
                throw new MessageRecorderException(MessageFormat.format(
                        "Config {0} has multiple bean type:{1}, {2}.",
                        item.getName(), item.getType(), beanConfigs.get(item.getName()).getType()));
            }
            beanConfigs.put(item.getName(), item);
        });

        beans.clear();
    }

    public String getDefaultBeanName() {
        return beanConfig.getDefaultItem();
    }

    public C getConfig(String name) {
        String beanName = StringUtils.hasText(name) ? name : beanConfig.getDefaultItem();
        C beanConfigItem = beanConfigs.get(beanName);
        if (beanConfigItem == null) {
            throw new MessageRecorderException(MessageFormat.format("Can''t find bean config:{0}.", beanName));
        }
        return beanConfigItem;
    }

    public B createBean(String name) {
        C beanConfigItem = getConfig(name);
        return createBean(beanConfigItem);
    }

    public B createBean(C beanConfigItem) {
        return configBeanHelper.createBean(beanConfigItem);
    }

    public B shareBean(String name) {
        return beans.computeIfAbsent(name, n -> createBean(name));
    }

    public FactoryInitializer<A, C, B> initializer() {
        return new FactoryInitializer<>(this);
    }

    public Collection<C> allConfigs() {
        return beanConfigs.values();
    }

    public Collection<C> configs(List<String> names) {
        if (ListUtils.isEmpty(names)) {
            return beanConfigs.values().stream()
                    .filter(BeanConfigItem::getEnabled)
                    .collect(Collectors.toList());
        }
        return names.stream()
                .map(beanConfigs::get)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    public static class FactoryInitializer<A extends BeanConfig<C>, C extends BeanConfigItem, B> {
        private final DynBeanFactory<A, C, B> factory;
        private final Map<String, Set<Class>> configTypesMap = new HashMap<>();
        private final Set<String> beanTypePackages = new HashSet<>();
        private String configFile;

        public FactoryInitializer(DynBeanFactory<A, C, B> factory) {
            this.factory = factory;
        }

        public FactoryInitializer<A, C, B> withConfigFile(String configFile) {
            this.configFile = configFile;
            return this;
        }

        public FactoryInitializer<A, C, B> withConfigType(Class packageSampleType, Class... configTypes) {
            configTypesMap.computeIfAbsent(packageSampleType.getPackage().getName(), k -> new HashSet<>())
                    .addAll(Arrays.asList(configTypes));
            return this;
        }

        public FactoryInitializer<A, C, B> withConfigType(String packageName, Class... configTypes) {
            configTypesMap.computeIfAbsent(packageName, k -> new HashSet<>())
                    .addAll(Arrays.asList(configTypes));
            return this;
        }

        public FactoryInitializer<A, C, B> withBeanType(Class... beanTypes) {
            for (Class beanType : beanTypes) {
                beanTypePackages.add(beanType.getPackage().getName());
            }
            return this;
        }

        public FactoryInitializer<A, C, B> withBeanType(String... beanTypePackages) {
            this.beanTypePackages.addAll(Arrays.asList(beanTypePackages));
            return this;
        }

        public void init() {
            configTypesMap.forEach((packageName, configTypes) -> {
                factory.initConfigType(packageName, configTypes.toArray(new Class[0]));
            });
            factory.initBeanTypes(beanTypePackages.toArray(new String[0]));
            if (StringUtils.hasText(configFile)) {
                factory.loadConfig(configFile);
            } else {
                log.warn("Config file is required.{}", this.getClass());
            }
        }
    }
}
